/**
 * 
 */
package org.statewalker.core;

import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.Stack;

import org.statewalker.core.IState.ICallback;
import org.statewalker.graph.GraphIterator;

public class Core {

    private static class ProcessHandler
        implements
        ILock,
        IStateExecutor,
        Comparable<ProcessHandler>,
        Runnable {

        private ICallback fCallback;

        private Core fCore;

        private int fCounter;

        private ProcessDescriptor fDescriptor;

        private String fName;

        private int fPriority;

        private Process fProcess;

        private Stack<IStateExecutor> fStack = new Stack<IStateExecutor>();

        private boolean fTerminated;

        public ProcessHandler(
            String processName,
            String rootStateName,
            Core core,
            ProcessDescriptor descriptor,
            IProcessContext context,
            ICallback callback,
            int priority) {
            fPriority = priority;
            fName = processName;
            fDescriptor = descriptor;
            fCore = core;
            fCallback = callback;
            fProcess = fDescriptor.newProcess(
                rootStateName,
                context,
                this,
                this,
                GraphIterator.ALL);
            fProcess.setEvent(new SimpleEvent(""));
            fCore.onProcessCreated(fName, fProcess);
        }

        public void activateState(Process process, IState state, IEvent event) {
            IStateExecutor executor = pushStateExecutor(state);
            executor.activateState(process, state, event);
        }

        public int compareTo(ProcessHandler o) {
            return o.fPriority - fPriority;
        }

        public void deactivateState(Process process, IState state, IEvent event) {
            IStateExecutor executor = popStateExecutor();
            fTerminated = fStack.empty();
            executor.deactivateState(process, state, event);
            if (process.isFinished()) {
                fCallback.finish(event);
                fCore.onProcessDestroyed(fName, fProcess);
            }
        }

        private void incCounter() {
            fCounter++;
            if (fCounter >= 2) {
                fCounter = 0;
                schedule();
            }
        }

        public void lock() {
            fCounter = 0;
        }

        private synchronized IStateExecutor popStateExecutor() {
            IStateExecutor executor = fStack.pop();
            return executor;
        }

        private IStateExecutor pushStateExecutor(IState state) {
            String stateKey = state.getKey();
            String processorKey = fDescriptor.getProcessorKey(stateKey);
            IStateExecutor executor = processorKey != null ? fCore
                .getStateExecutor(processorKey) : null;
            if (executor == null) {
                executor = !fStack.isEmpty() ? fStack.peek() : null;
                if (executor == null) {
                    executor = fCore.getDefaultStateExecutor();
                }
            }
            fStack.push(executor);
            return executor;
        }

        public void run() {
            fProcess.next();
        }

        public void schedule() {
            if (!fTerminated) {
                fCore.enqueue(this);
            }
        }

        public void unlock() {
            incCounter();
        }

        public void waitForUnlock() {
            incCounter();
        }

    }

    private IStateExecutor fDefaultStateExecutor;

    private Map<String, IStateExecutor> fExecutorsMap = new HashMap<String, IStateExecutor>();

    private int fProcessCounter;

    private Map<String, ProcessDescriptor> fProcessDescriptors = new HashMap<String, ProcessDescriptor>();

    private final Queue<Runnable> fSchedulerQueue;

    public Core(Queue<Runnable> queue, IStateExecutor defaultStateExecutor) {
        fDefaultStateExecutor = defaultStateExecutor;
        fSchedulerQueue = queue;
    }

    public synchronized void addProcessDescriptor(
        String processName,
        ProcessDescriptor descriptor) {
        fProcessDescriptors.put(processName, descriptor);
    }

    protected Runnable dequeue() {
        try {
            return fSchedulerQueue.poll();
        } catch (Exception e) {
            handleError(e);
            return null;
        }
    }

    protected void enqueue(Runnable runContext) {
        try {
            while (!fSchedulerQueue.offer(runContext)) {
                waitForQueue(runContext);
            }
        } catch (Exception e) {
            handleError(e);
        }
    }

    public IStateExecutor getDefaultStateExecutor() {
        return fDefaultStateExecutor;
    }

    public synchronized ProcessDescriptor getProcessDescriptor(
        String processName) {
        return fProcessDescriptors.get(processName);
    }

    public IStateExecutor getStateExecutor(String processorKey) {
        return fExecutorsMap.get(processorKey);
    }

    protected void handleError(Exception e) {
        throw new Error(e);
    }

    public boolean hasRunningProcesses() {
        return fProcessCounter > 0 || !isQueueEmpty();
    }

    protected boolean isQueueEmpty() {
        return fSchedulerQueue.isEmpty();
    }

    public ProcessDescriptor newProcessDescriptor(String processName)
        throws Exception {
        ProcessDescriptor descriptor = new ProcessDescriptor();
        addProcessDescriptor(processName, descriptor);
        return descriptor;
    }

    public void onProcessCreated(String name, Process process) {
        fProcessCounter++;
    }

    public void onProcessDestroyed(String name, Process process) {
        fProcessCounter--;
    }

    public synchronized void removeProcessDescriptor(String processName) {
        fProcessDescriptors.remove(processName);
    }

    public synchronized void removeStateExecutor(String processorKey) {
        Map<String, IStateExecutor> map = new HashMap<String, IStateExecutor>(
            fExecutorsMap);
        if (map.remove(processorKey) != null) {
            fExecutorsMap = map;
        }
    }

    public synchronized void setStateExecutor(
        String processorKey,
        IStateExecutor executor) {
        Map<String, IStateExecutor> map = new HashMap<String, IStateExecutor>(
            fExecutorsMap);
        map.put(processorKey, executor);
        fExecutorsMap = map;
    }

    public void startProcess(
        String processName,
        IProcessContext context,
        ICallback callback) {
        startProcess(processName, "Main", context, 10, callback);
    }

    public void startProcess(
        String processName,
        String rootStateName,
        IProcessContext context,
        int priority,
        ICallback callback) {
        Throwable error = null;
        try {
            ProcessDescriptor descriptor = getProcessDescriptor(processName);
            if (descriptor == null) {
                throw new IllegalArgumentException(
                    "There is no process description with such a name. Process name: "
                        + processName);
            }
            ProcessHandler handler = new ProcessHandler(
                processName,
                rootStateName,
                this,
                descriptor,
                context,
                callback,
                priority);
            handler.schedule();
        } catch (Throwable t) {
            error = t;
        } finally {
            if (error != null) {
                callback.finish(new ErrorEvent(error));
            }
        }
    }

    public boolean tick() {
        Runnable handler = dequeue();
        boolean result = handler != null;
        if (result) {
            handler.run();
        }
        return result;
    }

    protected void waitForQueue(Runnable runContext) {
    }

}